@wonseok-han/confluence-sync 2.0.0 → 2.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -63,7 +63,7 @@ export async function runStatus(argv) {
63
63
  console.log(` 원격에서 복구: ${cyan(`csync restore ${JSON.stringify(row.path)}`)}`);
64
64
  }
65
65
  }
66
- if (group === 'conflict' || group === 'attention')
66
+ if (group === 'conflict' || group === 'attention' || group === 'receive' && row.remoteReasons?.some(reason => reason.includes('위치 변경')))
67
67
  for (const reason of row.remoteReasons ?? [])
68
68
  console.log(dim(` ${reason}`));
69
69
  }
@@ -167,7 +167,7 @@ export async function runConvert(argv) {
167
167
  }
168
168
  const allMd = collectMarkdown(baseDir); // 링크 해석용(전체)
169
169
  const mdRels = allMd.map((f) => posix(relative(baseDir, f)));
170
- const vault = buildVault(mdRels, collectAssets(baseDir).map((f) => posix(relative(baseDir, f))));
170
+ const vault = buildVault(mdRels, collectAssets(baseDir).map((f) => posix(relative(baseDir, f))), baseDir);
171
171
  // obsidian 방향의 이름 중복 검사도 트리 전체 기준이어야 한다
172
172
  const nameCount = new Map();
173
173
  for (const f of allMd) {
@@ -17,7 +17,7 @@ const FENCE = /^\s*(?:```|~~~)/;
17
17
  * 대상이 **비어 있을 수 있다** — `[[#헤딩]]` 은 "이 문서의 이 섹션"이라는 Obsidian 표기다.
18
18
  * 표 안에서는 별칭 앞 파이프가 `\|` 로 이스케이프되므로 그 백슬래시도 함께 받아 준다.
19
19
  */
20
- export const WIKILINK_RE = /(!?)\[\[([^[\]|#]*)(#[^[\]|]+)?(?:\\?\|([^[\]]*))?\]\]/g;
20
+ export const WIKILINK_RE = /(!?)\[\[((?:(?!\[\[|\]\])[^|#\n])*)(#(?:(?!\[\[|\]\])[^|\n])+)?(?:\\?\|([^[\]]*))?\]\]/g;
21
21
  /**
22
22
  * WIKILINK_RE 의 캡처 그룹을 해석한다. 대상도 섹션도 없으면(`[[]]`) 링크가 아니므로 null.
23
23
  * 표의 `\|` 때문에 앞 그룹 끝에 딸려온 백슬래시는 여기서 떼어낸다.
@@ -98,7 +98,7 @@ export function resolveAnchorTargets(docs, vault, localTarget) {
98
98
  let slug;
99
99
  if (la.wiki) {
100
100
  // dest 가 비면 `[[#헤딩]]` — 같은 문서의 섹션이다
101
- target = la.dest === '' ? rel : (vault.notes[la.dest.toLowerCase().replace(/\.md$/i, '')] ?? null);
101
+ target = la.dest === '' ? rel : vaultPath(vault, la.dest, false, rel);
102
102
  slug = wikilinkAnchorToSlug(la.fragment);
103
103
  }
104
104
  else {
@@ -124,30 +124,45 @@ export function resolveAnchorTargets(docs, vault, localTarget) {
124
124
  * Obsidian 은 `[[파일명]]`(짧은 이름)과 `[[폴더/파일명]]`(경로) 을 모두 허용하므로 둘 다 등록한다.
125
125
  * 짧은 이름이 겹치면 경로가 사전순으로 앞서는 쪽이 이긴다(Obsidian 의 "가장 가까운 것" 규칙과는 다르지만 결정적이다).
126
126
  */
127
- export function buildVault(mdRels, assetRels) {
127
+ export function buildVault(mdRels, assetRels, base) {
128
128
  const index = (rels, stripExt) => {
129
129
  const idx = {};
130
130
  for (const rel of [...rels].sort()) {
131
131
  const withoutExt = stripExt ? rel.replace(/\.md$/i, '') : rel;
132
132
  for (const key of [withoutExt, basename(withoutExt)]) {
133
- const k = key.toLowerCase();
133
+ const k = key.normalize('NFC').toLowerCase();
134
134
  if (!(k in idx))
135
135
  idx[k] = rel; // 먼저 등록된(사전순 앞선) 것이 이긴다
136
136
  }
137
137
  }
138
138
  return idx;
139
139
  };
140
- return { notes: index(mdRels, true), assets: index(assetRels, false) };
140
+ return { notes: index(mdRels, true), assets: index(assetRels, false), rootName: base ? basename(base).normalize('NFC').toLowerCase() : undefined };
141
+ }
142
+ /** Resolve a vault-root path without reading outside the selected workspace. */
143
+ export function vaultPath(vault, target, embed = false, from = '') {
144
+ let key = target.normalize('NFC').toLowerCase();
145
+ if (key.startsWith('/') || key.includes('\\') || key.split('/').some(part => part === '..' || part === '.'))
146
+ return null;
147
+ const noteKey = key.replace(/\.md$/i, '');
148
+ const dir = dirname(from).normalize('NFC').toLowerCase();
149
+ const nearby = !key.includes('/') && dir !== '.' ? vault.notes[dir + '/' + noteKey] : undefined;
150
+ const direct = embed ? (vault.assets[key] ?? vault.notes[noteKey]) : (nearby ?? vault.notes[noteKey] ?? vault.assets[key]);
151
+ if (direct)
152
+ return direct;
153
+ if (!vault.rootName)
154
+ return null;
155
+ const parts = key.split('/'), at = parts.lastIndexOf(vault.rootName);
156
+ if (at < 0)
157
+ return null;
158
+ key = parts.slice(at + 1).join('/');
159
+ return embed ? (vault.assets[key] ?? vault.notes[key.replace(/\.md$/i, '')] ?? null) : (vault.notes[key.replace(/\.md$/i, '')] ?? vault.assets[key] ?? null);
141
160
  }
142
161
  /** 파일(rel) 기준으로 wikilink 대상을 상대경로로 풀어주는 resolver. */
143
162
  export function vaultResolver(rel, vault) {
144
163
  const dir = dirname(rel) === '.' ? '' : dirname(rel);
145
164
  return (target, embed) => {
146
- const key = target.toLowerCase().replace(/\.md$/i, '');
147
- // 임베드(![[...]])는 대개 이미지, 링크([[...]])는 대개 노트 → 각자 먼저 찾아본다
148
- const hit = embed
149
- ? (vault.assets[target.toLowerCase()] ?? vault.notes[key])
150
- : (vault.notes[key] ?? vault.assets[target.toLowerCase()]);
165
+ const hit = vaultPath(vault, target, embed, rel);
151
166
  if (!hit)
152
167
  return null;
153
168
  const r = relative(dir || '.', hit).split('\\').join('/');
@@ -46,7 +46,7 @@ const markdownDisplayText = (text) => text.replace(/[\\_]/g, '\\$&');
46
46
  * 코드블록(``` / ~~~)과 인라인 코드(`...`) 안은 그대로 둔다 — 링크처럼 생긴 예제 코드를
47
47
  * 망가뜨리지 않기 위해서다.
48
48
  */
49
- function mapProse(md, fn) {
49
+ export function mapProse(md, fn) {
50
50
  let inFence = false;
51
51
  return md
52
52
  .split('\n')